/*
   Copyright (c) 2008, Andrii Olefirenko
   All rights reserved.
   New BSD License
 */
package framework
{
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IEventDispatcher;
    import flash.utils.Dictionary;
    import flash.utils.Proxy;
    import flash.utils.describeType;
    import flash.utils.flash_proxy;
    
    import mx.events.PropertyChangeEvent;
    import mx.events.PropertyChangeEventKind;
    import mx.rpc.AsyncToken;
    import mx.utils.DescribeTypeCache;
    import mx.utils.DescribeTypeCacheRecord;
    import mx.utils.ObjectUtil;

    /**
     * This event is dispatched by AOPCtxCmd. Add AOPCtxCmd bean <code>Ctx.getInstance().addBean(new AOPCtxCmd);</code> to enable this event.
     * Dispatched before invocation of <code>act()</code> method of the bean. You can cancel the invocation (use <code>event.preventDefault()</code>)
     * Use <code>actReplacement</code> property to replace the called function
     * @eventType framework.AOPEvent.BEFORE_ACT
     */
    [Event(name="before_act", type = "framework.AOPEvent")]

    /**
     * This event is dispatched by AOPCtxCmd. Add AOPCtxCmd bean <code>Ctx.getInstance().addBean(new AOPCtxCmd);</code> to enable this event.
     * Dispatched after invocation of <code>act()</code> method of the bean. This event is not dispatched If the invocation was canceled.
     * @eventType framework.AOPEvent.AFTER_ACT
     */
    [Event(name="after_act", type = "framework.AOPEvent")]

    /**
     * This event is dispatched by AOPCtxCmd. Add AOPCtxCmd bean <code>Ctx.getInstance().addBean(new AOPCtxCmd);</code> to enable this event.
     * Dispatched when the bean is accessed as a property.
     * @eventType framework.AOPEvent.BEAN_ACCESS
     */
    [Event(name="bean_access", type = "framework.AOPEvent")]

    /**
     * This event is dispatched by Ctx. This is used for databinding mainly.
     * Dispatched when the bean is added.
     * @eventType mx.events.PropertyChangeEvent.PROPERTY_CHANGE
     */
    [Event(name="propertyChange", type = "mx.events.PropertyChangeEvent")]

    [Bindable(event="propertyChange")]
    /**
     * This is the main class of Ctx framework. This is a container for beans. There could be many instances of
     * this class representing different contexts. There are several ways to access instances of this class
     * <pre>
     * //standard
     * var c:Ctx = new Ctx(); // or
     * var otherCtx:Ctx = new Ctx("myOtherCtx");
     * </pre>
     *
     * To add a new bean, use following methods
     * <pre>
     * var c:Ctx = new Ctx;
     * c.addBean(new MyBean, "mybean");
     * c.addBean(new MyBean);
     * c.addBean(MyBean);  // this will add the bean lazily
     * c.addBean(MyBean,"mybean","alternativeName");
     * c.myBean = new MyBean();
     * c.myClassBean = MyBean;
     * </pre>
     *
     */
    dynamic public class Ctx extends Proxy implements IEventDispatcher
    {

        //internal
        static internal var contexts:Dictionary = new Dictionary(true);

        static internal function getCtx(ctxName:String, facade:Ctx):Ctx
        {
            var c:Ctx = contexts[ctxName];
            if (!c)
            {
                c = new Ctx(ctxName);
                c.beans = new Dictionary(true);
                c.beanConfigurators = new Dictionary(true);
                c._dispatcher = new EventDispatcher(c);
                c.beans.c = facade;
                contexts[ctxName] = c;
            }
            return c;
        }

        public function Ctx(ctxName:String = "default")
        {
            _ctxName = ctxName;
        }

        internal var beans:Dictionary;

        internal var beanConfigurators:Dictionary;

        internal var _dispatcher:EventDispatcher;

        private var nextNameArray:Array;

        private var _ctxName:String;

        public function get ctxName():String
        {
            return _ctxName;
        }

        public function set ctxName(val:String):void
        {
            if (_ctxName == val)
                return;
            _ctxName = val;
            dispatchEvent(new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, false, false, PropertyChangeEventKind.UPDATE));
        }

        private function get ctx():Ctx
        {
            return getCtx(_ctxName, this);
        }

        public function addBean(instance:*, ... names):void
        {
            ctx._addBean.apply(ctx, [ instance ].concat(names));
        }

        public function removeBean(... names):void
        {
            ctx._removeBean.apply(ctx, names);
        }

        public function get configurator():Dictionary
        {
            return ctx.beanConfigurators;
        }

        internal function _addBean(instance:*, ... names):void
        {
            if (!instance)
                return;
            var configurator:Object;
            if (names.length > 1)
            {
                if (!(names[0] is String))
                {
                    configurator = names[0];
                    names.shift();
                }
            }
            if (!names.length)
            {
                names.push(extractName(instance.toString()));
            }
            for (var i:uint = 0; i < names.length; i++)
            {
                var name:String = names[i];
                if (beans[name])
                {
                    throw new Error("Bean " + name + " is already added");
                }
                beans[name] = instance;
                if (configurator)
                {
                    beanConfigurators[name] = configurator;
                }
            }

            if (!ObjectUtil.isSimple(instance))
            {
                var props:Object = _describeInstance(instance);
                _injectBeans(instance);
                _injectConfigurator(instance, names[0]);
                if (instance is IEventDispatcher)
                {
                    IEventDispatcher(instance).dispatchEvent(new Event(Event.INIT));
                }
                if (props.hasInit)
                {
                    _initBean(instance);
                }
                if (props.hasAct)
                {
                    for each (name in names)
                    {
                        addEventListener(name, _commandListener, false, 0, true);
                    }
                }
            }
            dispatchEvent(new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, false, false, PropertyChangeEventKind.UPDATE, names[0], null, instance));
        }

        internal function _describeInstance(bean:Object):Object
        {
            var r:Object = {};
            var x:XML = describeType(bean);
            if (bean is Class)
            {
                x = x.factory[0];
            }
            else
            {
                r.hasInit = x.method.(@name == 'initBean').length() == 1;
            }
            r.hasAct = x.method.(@name == 'act').length() == 1;
            return r;
        }

        internal function _removeBean(... beanNames):void
        {
            for each (var b:Object in beanNames)
            {
                if (b is String)
                {
                    _deleteProperty(b);
                }
                else if (b is Class)
                {
                    for (var s:String in beans)
                    {
                        if (!(beans[s] is Ctx) && (beans[s] is Class(b) || beans[s] == b))
                        {
                            _deleteProperty(s);
                        }
                    }
                }
                else
                {
                    for (s in beans)
                    {
                        if (b == beans[s])
                        {
                            _deleteProperty(s);
                        }
                    }
                }
            }
        }

        internal function _deleteProperty(name:*):Boolean
        {
            if (!beans[name])
                return false;
            removeEventListener(name, _commandListener, false);
            var r:Boolean = delete beans[name];
            if (r)
                dispatchEvent(new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, false, false, PropertyChangeEventKind.DELETE, name));
            return r;
        }

        override flash_proxy function deleteProperty(name:*):Boolean
        {
            return ctx._deleteProperty(name);
        }

        internal function _commandListener(e:Event):void
        {
            if (e is CtxEvent)
            {
                var ctxE:CtxEvent = CtxEvent(e);
                contexts[_ctxName]._getProperty(e.type)[ctxE.commandName](e);
            }
            else
            {
                contexts[_ctxName]._callProperty(e.type, e);
            }
        }

        private function _initBean(instance:Object):void
        {
            try
            {
                instance.initBean();
            }
            catch (e:ArgumentError)
            {
                if (e.errorID == 1063)
                {
                    instance.initBean(this);
                }
                else
                {
                    throw e;
                }
            }
        }

        public function injectBeans(bean:Object):void
        {
            ctx._injectBeans(bean);
        }

        internal function _injectBeans(bean:Object):void
        {
            if (bean is Ctx)
                return;
            var beanNames:Array = [];
            for (var s:String in beans)
            {
                beanNames.push(s);
            }
            for each (s in beanNames)
            {
                _injectAutowire(bean, s);
            }
        }

        internal function _injectAutowire(hostBean:Object, property:String):void
        {
            if (property in hostBean)
            {
                var obj:* = _getProperty(property);
                if (obj != hostBean[property])
                {
                    hostBean[property] = obj;
                }
            }

        }

        internal function _injectConfigurator(bean:Object, name:String):void
        {
            var configurator:Object = beanConfigurators[name];
            if (!configurator)
                return;
            var classInfo:DescribeTypeCacheRecord = DescribeTypeCache.describeType(configurator);
            var isDynamic:Boolean = classInfo.typeDescription.@isDynamic == "true";
            if (isDynamic)
            {
                for (var s:String in configurator)
                {
                    if (s in bean)
                    {
                        bean[s] = configurator[s];
                    }
                }
            }
            for each (var v:XML in classInfo.typeDescription..accessor)
            {
                var n:String = v.@name;
                if (String(v.@access).indexOf("read") != -1 && n in bean)
                {
                    bean[n] = configurator[n];
                }
            }
            for each (v in classInfo.typeDescription..variable)
            {
                n = v.@name;
                if (n in bean)
                {
                    bean[n] = configurator[n];
                }
            }

        }

        internal function _hasProperty(name:*):Boolean
        {
            return name in beans;
        }

        override flash_proxy function hasProperty(name:*):Boolean
        {
            return ctx._hasProperty(name);
        }

        internal function _getProperty(name:*):*
        {
            var bean:Object = beans[name];
            if (!bean)
            {
                return undefined;
            }
            if (bean is Class)
            {
                var beanClazz:Class = Class(bean);
                try
                {
                    bean = beanClazz(new beanClazz);
                }
                catch (e:Error)
                {
                    throw new Error("Error creating bean. No default constructor?");
                }

                _deleteProperty(name);
                _addBean(bean, name);
                return bean;
            }
            if (!ObjectUtil.isSimple(bean))
            {
                //doing injection again
                _injectBeans(bean);
                _injectConfigurator(bean, name);
            }
            return bean;
        }

        override flash_proxy function getProperty(name:*):*
        {
            return ctx._getProperty(name);
        }

        internal function _callProperty(name:*, ... args:Array):*
        {
            var bean:* = _getProperty(name);
            if (bean is Function)
            {
                return bean.apply(this, args);
            }
            else
            {
                return bean.act.apply(this[name], args);
            }
        }

        override flash_proxy function callProperty(name:*, ... args:Array):*
        {
            return ctx._callProperty.apply(ctx, [ name ].concat(args));
        }

        internal function _setProperty(name:*, value:*):void
        {
            if (beans[name] == value)
                return;
            _deleteProperty(name);
            _addBean(value, name);
        }

        override flash_proxy function setProperty(name:*, value:*):void
        {
            ctx._setProperty(name, value);
        }

        public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void
        {
            ctx._dispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference);
        }

        public function dispatchEvent(event:Event):Boolean
        {
            return ctx._dispatcher.dispatchEvent(event);
        }

        public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void
        {
            ctx._dispatcher.removeEventListener(type, listener, useCapture);
        }

        public function hasEventListener(type:String):Boolean
        {
            return ctx._dispatcher.hasEventListener(type);
        }

        public function willTrigger(type:String):Boolean
        {
            return ctx._dispatcher.willTrigger(type);
        }

        override flash_proxy function nextNameIndex(index:int):int
        {
            if (index == 0)
            {
                nextNameArray = [];
                for (var op:String in ctx.beans)
                {
                    nextNameArray.push(op);
                }
            }
            return index < nextNameArray.length ? index + 1 : 0;
        }

        override flash_proxy function nextName(index:int):String
        {
            return nextNameArray[index - 1];
        }

        override flash_proxy function nextValue(index:int):*
        {
            return ctx.beans[nextNameArray[index - 1]];
        }

        private function extractName(s:String):String
        {
            if (s.indexOf("[class ") == 0 || s.indexOf("[object ") == 0)
            {
                s = s.replace("[class ", "");
                s = s.replace("[object ", "");
                s = s.substr(0, s.length - 1);
                s = s.charAt(0).toLowerCase() + s.substr(1);
            }
            return s;
        }

        public function handle(asyncToken:AsyncToken, resultHandler:Function, faultHandler:Function = null):AsyncToken
        {
            return framework.handle(asyncToken, resultHandler, faultHandler);
        }

        public function chain(... args):AsyncToken
        {
            return framework.chain.apply(this, args);
        }

        public function handleData(asyncToken:AsyncToken, property:String, transform:Function = null):AsyncToken
        {
            return framework.handleData(asyncToken, this, property, transform);
        }




    }
}
